package com.wtone.platform.gat.images.gat.config;

import com.wtone.platform.gat.images.gat.entity.GatUser;
import com.wtone.platform.gat.images.gat.mapper.GatUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;

import java.util.List;

/**
 * @Author: aiLin
 * @Date: 2020/9/27 16:59
 * @desc: digest摘要认证过滤器 （对所有请求都会进行拦截）
 * <dependency>
 * 	  <groupId>org.springframework.boot</groupId>
 * 	  <artifactId>spring-boot-starter-security</artifactId>
 * </dependency>
 * 参考： 服务端：https://www.jianshu.com/p/48c348bfb0ad， https://blog.csdn.net/wangb_java/article/details/86714105
 * 参考：客户端：https://www.jianshu.com/p/564ad28d9c51 ， https://www.jianshu.com/p/cf5a900d4ef7
 *
 * 第一次请求：
 *
 * 401 Unauthorized
 *
 * WWW-Authenticate: Digest realm="myrealm", qop="auth", nonce="MTYwMTE5OTE3MDI3OToxNmQ5YTI4NGVkNmU3M2NkMjI3ODkzNzExYWEyNjQzMA=="
 *
 * Digest：指摘要认证。
 * qop：该值决定了生成摘要的具体计算公式，及参与计算的参数。本篇仅以spring security默认公式为例。
 * nonce：相当于随机数。计算公式为base64(时间戳:md5(时间戳:key))。首先由时间戳和后台配置的key值，生成md5散列值，再用时间戳和md5值进行base64编码。
 * 之后客户端登录请求头都必须有nonce参数，服务端收到nonce，
 * 解码出时间戳，将其和服务端的key重新生成nonce，比对一致，由此证明请求头的nonce就是服务端原始发放的nonce，请求合法。
 * 时间戳：服务端从nonce解码的时间戳，还会和当前时间进行比较。如果超时则验证失败，并刷新nonce放在响应头，同时增加一个响应参数stale=true。浏览器根据该参数，
 * 自动使用新的nonce重新发起认证请求，整个过程用户无感知。spring security中默认超时为5分钟，如果黑客拦截到用户认证参数，最多只能有效利用5分钟，即使他刷新nonce，
 * 不知道用户密码是无法重新认证的，下面会讲。
 *
 * 理论上md5散列值是不可逆的，所以黑客无法擅改nonce中的时间戳，否则会造成服务端验证nonce失败。又因为黑客不知道服务端key，所以其无法伪造nonce
 *
 * 第二次请求：
 *
 *Digest username="javaboy",
 * realm="myrealm",
 * qop="auth",
 * nonce="MTYwMTE5OTU4NjQ1NTo1MzI2ZDMzZDEwY2E2NWEwMDZjZTFlNGUwZGNjMGIxYw==",
 * uri="/test",
 * nc="00000001",
 * cnonce="97wv9648",
 * response="71bff182cb239b7ac0d0cc5d5a1e09c7"
 *
 * 响应：
 * {"msg":"success","code":200,"data":"hello,你好我是Digests 服务端!欢迎你！"}
 *
 * Digest：摘要认证类型。
 * username：登录时输入的用户名。
 * realm：这是之前服务器响应的参数，原样返回，上一篇讲过。。。
 * nonce：同上，并且之后每次请求，都会用上面的方法对其进行认证。
 * uri：可在后台比对实际请求路径是否一致，否则有被黑客攻击的可能性。
 * response：将所有Authorization参数，以及用户输入的密码等，共同生成的md5摘要。服务端收到请求，会用同样的方法，
 * 并查询数据库中的用户密码再次生成摘要进行对比，只要有一个参数被擅改，结果都不一致。所以不知道用户密码，是无法生成一致摘要的。
 * qop：源自服务器
 * nc：16进制数，使用当前nonce请求次数，服务端可对其进行限制，达到一定次数主动刷新nonce，以防止被黑客利用。
 * cnonce：客户端生成的随机数，也会参与摘要计算，使得每次请求的response值都不一样。服务端可以保存每次请求的cnonce，如果发现其值已经存在，很可能是黑客攻击。
 *
 * */
@Slf4j
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    //用户名
    @Value("${digests.userName}")
    private String userName;

    //密码
    @Value("${digests.password}")
    private String password;

    @Autowired
    private GatUserMapper gatUserMapper;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .csrf()
                .disable()
                .exceptionHandling()
                .authenticationEntryPoint(digestAuthenticationEntryPoint())
                .and()
                .addFilter(digestAuthenticationFilter());
    }

    @Bean
    DigestAuthenticationEntryPoint digestAuthenticationEntryPoint() {
        DigestAuthenticationEntryPoint entryPoint = new DigestAuthenticationEntryPoint();
        entryPoint.setKey("javaboy");//加密key
        entryPoint.setRealmName("wtone GAT1400");
        entryPoint.setNonceValiditySeconds(300);//有效期 相当于随机数。计算公式为base64(时间戳:md5(时间戳:key))。
        // 首先由时间戳和后台配置的key值，生成md5散列值，再用时间戳和md5值进行base64编码。之后客户端登录请求头都必须有nonce参数，服务端收到nonce，
        // 解码出时间戳，将其和服务端的key重新生成nonce，比对一致，由此证明请求头的nonce就是服务端原始发放的nonce，请求合法。
        //时间戳：服务端从nonce解码的时间戳，还会和当前时间进行比较。如果超时则验证失败，并刷新nonce放在响应头，同时增加一个响应参数stale=true。
        // 浏览器根据该参数，自动使用新的nonce重新发起认证请求，整个过程用户无感知。spring security中默认超时为5分钟，
        // 如果黑客拦截到用户认证参数，最多只能有效利用5分钟，即使他刷新nonce，不知道用户密码是无法重新认证的，下面会讲。

        return entryPoint;
    }
    @Bean
    DigestAuthenticationFilter digestAuthenticationFilter() {
        DigestAuthenticationFilter filter = new DigestAuthenticationFilter();
        filter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint());
        filter.setUserDetailsService(userDetailsService());
        return filter;
    }

    @Override
    @Bean
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        GatUser gatUser = new GatUser();
        List<GatUser> gatUsers = gatUserMapper.selectGatUserList(gatUser);
        if(gatUsers==null || gatUsers.size()<=0){
            log.info("系统用户列表，初始化>>>");
            manager.createUser(User.withUsername(userName).password(password).roles("admin").build());
            return manager;
        }
        log.info("数据库查询用户列表，初始化>>>");
        for(int i=0;i<gatUsers.size();i++){
            GatUser user = gatUsers.get(i);
            manager.createUser(User.withUsername(user.getUserName()).password(user.getUserPassword()).roles("admin").build());
        }
        return manager;
    }


    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/subscribe/**","/doc.html/**"); //无条件允许访问
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

}
